package osmedile.intellij.checkmyvars;

import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorAction;
import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import osmedile.intellij.util.psi.AnnotationUtil;
import osmedile.intellij.util.psi.PsiEquivalenceUti;
import osmedile.intellij.util.psi.PsiParameterComparator;
import osmedile.intellij.util.psi.PsiUtil;
import osmedile.intellij.util.psi.SimpleAllower;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Olivier Smedile
 * @version $Id$
 */
public class CheckAnnotedVars extends EditorAction {

    private CheckMyVarsAppComp cmvAppComp;


    public CheckAnnotedVars() {
        super(null);
        this.setupHandler(new EditorWriteActionHandler() {
            public void executeWriteAction(Editor editor, DataContext dataContext) {
                PsiFile psiFile = DataKeys.PSI_FILE.getData(dataContext);
                Project project = DataKeys.PROJECT.getData(dataContext);
                PsiJavaFile psiJavaFile;
                if (project != null && psiFile instanceof PsiJavaFile) {
                    psiJavaFile = (PsiJavaFile) psiFile;

                    boolean codeGenerated = actionOn(dataContext, editor,
                            PsiManager.getInstance(project).getElementFactory(), psiJavaFile);

                    //ask to create sample
                    if (!codeGenerated && getCmvAppComp().getConfig().getAskToCreateSample()) {
                        CreateSampleAction sampleAction = (CreateSampleAction) ActionManager.getInstance()
                                .getAction(AskCreateSampleAction.ACTION_ID);
                        sampleAction.doAction(project, dataContext);
                    }
                }
            }
        });
    }

    protected CheckMyVarsAppComp getCmvAppComp() {
        if (cmvAppComp == null) {
            cmvAppComp = ApplicationManager.getApplication().getComponent(CheckMyVarsAppComp.class);
        }
        return cmvAppComp;
    }

// --------------------- GETTER / SETTER METHODS ---------------------

    /**
     * Return list of available var checkers from configuration
     *
     * @return
     */
    public List<VarChecker> getVarCheckers() {
        return getCmvAppComp().getConfig().getVarCheckers();
    }

// --------------------- METHODS ---------------------


    /**
     * Search all classes on the specified java file on call actionOn.
     *
     * @param dataContext
     * @param editor
     * @param factory
     * @param psiJavaFile
     *
     * @return
     */
    public boolean actionOn(DataContext dataContext, Editor editor,
                            PsiElementFactory factory, PsiJavaFile psiJavaFile) {

        boolean codeGenerated = false;

        PsiClass[] psiClasses = psiJavaFile.getClasses();
        for (PsiClass psiClass : psiClasses) {
            codeGenerated |= actionOn(dataContext, editor, factory, psiClass);
        }

        return codeGenerated;
    }


    /**
     * Search all method and inner classes on the specified java class on call actionOn.
     *
     * @param dataContext
     * @param editor
     * @param factory
     * @param psiClass
     *
     * @return
     */
    public boolean actionOn(DataContext dataContext, Editor editor,
                            PsiElementFactory factory, PsiClass psiClass) {

        boolean codeGenerated = false;

        PsiMethod[] methods = psiClass.getMethods();
        for (PsiMethod method : methods) {
            codeGenerated |= actionOn(dataContext, editor, factory, method);
        }


        PsiClass[] inners = psiClass.getAllInnerClasses();

        for (PsiClass inner : inners) {
            codeGenerated |= actionOn(dataContext, editor, factory, inner);
        }
        return codeGenerated;
    }


    /**
     * @param dataContext
     * @param editor
     * @param factory
     * @param method
     *
     * @return
     */
    public boolean actionOn(DataContext dataContext, Editor editor,
                            PsiElementFactory factory, PsiMethod method) {

        final PsiCodeBlock body = method.getBody();
        //method with no body
        if (body == null) {
            return false;
        }

        boolean codeGenerated = false;

        PsiParameter[] params = method.getParameterList().getParameters();
        for (PsiParameter param : params) {
            for (VarChecker varChecker : getVarCheckers()) {
                if (varChecker.getActive() && varChecker.accept(param)) {

                    codeGenerated = true;
                    generateVarCheck(factory, method, param, varChecker);
                }
            }
        }
        return codeGenerated;
    }


    /**
     * Generate (or update) the code to check the specified parameter with the specified varChecker.
     *
     * @param factory
     * @param method
     * @param param
     * @param varChecker
     */
    public void generateVarCheck(PsiElementFactory factory, PsiMethod method, PsiParameter param,
                                 VarChecker varChecker) {

        Map<String, String> replacement = new HashMap<String, String>();

        //template with all variables replaced excepted them that
        // can't be refactored
        String templateInvariant = transformInvariant(method, param,
                varChecker.getTemplate(), replacement);

        //template with all variables replaced
        String template = transform(templateInvariant, replacement);

        // Create code fragment with some variables replacement, this serves as a pattern to detect
        // existant code
        PsiCodeFragment codeFragment = factory.createCodeBlockCodeFragment(templateInvariant, method, false);


        SimpleAllower allower = new SimpleAllower(replacement.keySet());


        Map<PsiElement, PsiElement> allowed = new HashMap<PsiElement, PsiElement>();
        //Detect if code was already generated
        PsiElement[] result = PsiEquivalenceUti.findContainedPsi(method.getBody(), codeFragment,
                new PsiParameterComparator(), allowed, allower, false);

        if (result != null) {
            //TODO try to do like find/replace show text to replace and ask user
            //TODO add an option to navigate to element before replacing them

            for (PsiElement original : allowed.keySet()) {
                try {
                    String replacementText = allowed.get(original).getText();
                    replacementText =
                            transform(replacementText, replacement);

                    PsiElement ref;
                    try {
                        ref = factory.createExpressionFromText(replacementText, method);
                    } catch (IncorrectOperationException e) {
                        //code will be unformatted
                        ref = factory.createCodeBlockFromText(replacementText, method);
                    }


                    original.replace(ref);
                } catch (IncorrectOperationException e) {
                    throw new RuntimeException(e);
                }
            }
        } else {
            insertTemplate(factory, method, template);
        }
    }

    /**
     * Insert the template in the specified method.
     * Template is added at the beginning of the method.
     *
     * @param factory
     * @param method   the method (or constructor) in which to add the template
     * @param template the template of code to add
     */
    protected void insertTemplate(@NotNull PsiElementFactory factory, @NotNull PsiMethod method,
                                  @NotNull String template) {
        if (template == null) {
            throw new IllegalArgumentException("template must not be null");
        }
        if (method == null) {
            throw new IllegalArgumentException("method must not be null");
        }
        if (factory == null) {
            throw new IllegalArgumentException("factory must not be null");
        }
        try {
            PsiCodeBlock body = method.getBody();
            PsiElement code;
            try {
                code = factory.createStatementFromText(template, method);
            } catch (IncorrectOperationException e) {
                //code will be unformatted
                code = factory.createCodeBlockFromText("\n" + template, method);
            }
            if (body == null) {
                method.add(code);
            }

            boolean codeAdded = false;
            if (method.isConstructor()) {
                PsiElement thisOrSuper = PsiUtil.findThisOrSuper(method);
                if (thisOrSuper != null) {
                    body.addAfter(code, thisOrSuper);
                    codeAdded = true;
                }
            }

            if (!codeAdded) {
                if (body.getLBrace() != null) {
                    body.addAfter(code, body.getLBrace());
                } else {
                    body.add(code);
                }
            }
        } catch (IncorrectOperationException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Transform the specified template with all accessible variables except them which can change between two call of this action.
     * This include value of Annotation parameter.
     * Name of field, annotation and parameter are considered as handled by IDEA refactoring.
     *
     * @param method
     * @param parameter
     * @param template
     * @param replacement key: template parameter, value: annotation parameter value
     *
     * @return
     */
    public String transformInvariant(@NotNull PsiMethod method, @NotNull PsiParameter parameter,
                                     @NotNull String template, Map<String, String> replacement) {
        if (template == null) {
            throw new IllegalArgumentException("template must not be null");
        }
        if (parameter == null) {
            throw new IllegalArgumentException("parameter must not be null");
        }
        if (method == null) {
            throw new IllegalArgumentException("method must not be null");
        }
        String result = template;
        PsiAnnotation[] annos = parameter.getAnnotations();
        for (PsiAnnotation anno : annos) {
            //original annotation name
            final String annoName = anno.getNameReferenceElement().getText();
            //Variable name in template replaced by annotation name
            final String tplAnnoName = "A_" + annoName;

            //do not replace this directly because it can change between two generation
            //and will not be handled by refactoring
            Map<String, String> attributes = AnnotationUtil.findAnnoAttributes(anno);

            for (String attrName : attributes.keySet()) {
                replacement.put("$" + tplAnnoName + "_P_" + attrName + "$", attributes.get(attrName));
            }
        }

        result = result.replace("$CLASS$", method.getContainingClass().getName());
        result = result.replace("$METHOD$", method.getName());
        result = result.replace("$VAR$", parameter.getName());
        result =
                result.replace("$VAR_TYPE$",
                        parameter.getType().getDeepComponentType().getPresentableText().split("<")[0]);
        result = result.replace("$VAR_REAL_TYPE$", parameter.getType().getPresentableText());

        return result;
    }


    /**
     * Set all variables stored in replacement on the template.
     *
     * @param template
     * @param replacement
     * @return
     */
    public String transform(@NotNull String template, @NotNull Map<String, String> replacement) {
        if (replacement == null) {
            throw new IllegalArgumentException("replacement must not be null");
        }
        if (template == null) {
            throw new IllegalArgumentException("template must not be null");
        }
        String result = template;
        for (String key : replacement.keySet()) {
            result = result.replace(key, replacement.get(key));
        }

        return result;
    }
}
